/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.server.servlet.book.content;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Stack;
import javax.jcr.RepositoryException;
import javax.xml.parsers.ParserConfigurationException;
import offset.nodes.client.model.XmlReader;
import offset.nodes.server.servlet.book.Book;
import offset.nodes.server.servlet.book.provider.NodeProvider;
import offset.nodes.server.servlet.book.NameBuilder;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

public class PageParser extends EmptyFilterContentHandler implements ContentHandler {

    static final String CDATA = "CDATA";
    boolean content = false;
    boolean firstContentElement = false;
    Page page;
    Stack<String> path = new Stack<String>();
    static HashSet<String> hyperlinkParents = new HashSet<String>();
    StringBuffer charDestination = null;
    NodeProvider nodeProvider;
    HashMap<String, String> substitutions = null;
    Book book;
    boolean followLinks;

    static {
        hyperlinkParents.add("td");
    }

    public PageParser(Book book, ContentHandler target, NodeProvider nodeProvider, Page page, boolean followLinks) {
        super(target);
        this.page = page;
        this.nodeProvider = nodeProvider;
        this.book = book;
        this.followLinks = followLinks;
    }

    /**
     * Parse the HTML content and modify it.
     *
     * @param in
     * @throws Exception
     */
    protected void parse(InputStream in) throws SAXException, IOException, ParserConfigurationException {
        XmlReader reader = new XmlReader();
        reader.setContentHandler(this);
        reader.addPureInlineElement("img");
        reader.parse(new InputSource(in));
    }

    /**
     * Parse the HTML content and modify it.
     *
     * @param in
     * @throws Exception
     */
    public void parse(String in) throws SAXException, IOException, ParserConfigurationException {
        if (!book.isScriptingEnabled())
            in = discardElement(in, "script");
        parse(new ByteArrayInputStream(in.getBytes()));
    }

    protected String discardElement(String in, String elementName) {
        String elementStart = "<" + elementName;
        String elementEnd = "</" + elementName + ">";

        return in.replaceAll(elementStart + ".*" + elementEnd, "");
    }

    /**
     * Substitute the content of the element with some value supplied from external.
     * 
     * @param uri
     * @param localName
     * @param qName
     * @param atts
     * @throws SAXException
     */
    protected void processSubstitution(String uri, String localName, String qName, Attributes atts) throws SAXException {
        handler.startElement(uri, localName, qName, atts);

        if (substitutions == null)
            return;

        String id = getAttribute(atts, "id");
        if (!substitutions.containsKey(id))
            return;

        String value = substitutions.get(id);
        handler.characters(value.toCharArray(), 0, value.length());
        setVisible(false);
    }

    /**
     * Store references to CSS as HtmlResources. Modify the path to point to
     * resource directory.
     *
     * @param uri
     * @param localName
     * @param qName
     * @param atts
     * @throws SAXException
     */
    protected void processCSS(String uri, String localName, String qName, Attributes atts) throws SAXException {
        final String TYPE = "type";
        final String TYPE_CSS = "text/css";

        String type = getAttribute(atts, TYPE);
        if (type == null || !type.equals(TYPE_CSS)) {
            handler.startElement(uri, localName, qName, atts);
            return;
        }

        Resource resource = modifyReference(uri, localName, qName, atts, "href", new Substituter() {

            @Override
            public String substitute(String value) {
                return book.getNameBuilder().buildStyleSheetName(value);
            }
        });

        if (resource != null) {
            resource.setContent(new StringContent());
            page.addResource(resource);
        }
    }

    /**
     * Store references to scripts as HtmlResources. Modify the path to point to
     * resource directory.
     *
     * @param uri
     * @param localName
     * @param qName
     * @param atts
     * @throws SAXException
     */
    protected void processScript(String uri, String localName, String qName, Attributes atts) throws SAXException {
        final String TYPE = "type";
        final String TYPE_JAVASCRIPT = "text/javascript";

        String type = getAttribute(atts, TYPE);
        if (type == null || !type.equals(TYPE_JAVASCRIPT)) {
            handler.startElement(uri, localName, qName, atts);
            return;
        }

        Resource resource = modifyReference(uri, localName, qName, atts, "src", new Substituter() {

            @Override
            public String substitute(String value) {
                return book.getNameBuilder().buildStyleSheetName(value);
            }
        });

        if (resource != null) {
            resource.setContent(new StringContent());
            page.addResource(resource);
        }
    }

    /**
     * Store reference to images as HtmlResources. Modify the path to point
     * to the resource directory
     *
     * @param uri
     * @param localName
     * @param qName
     * @param atts
     * @throws SAXException
     */
    protected void processImage(String uri, String localName, String qName, Attributes atts) throws SAXException {
        Resource resource = modifyReference(uri, localName, qName, atts, "src", new Substituter() {

            @Override
            public String substitute(String value) {
                return book.getNameBuilder().buildImageName(value);
            }
        });

        if (resource != null) {
            resource.setContent(new BinaryContent());
            page.addResource(resource);
        }
    }

    /**
     * Return obviously invalid references
     *
     * @param href the references
     * @return is invalid or not?
     */
    protected boolean isInvalid(String href) {
        if (href == null || href.length() == 0 || href.equals("#"))
            return true;
        return false;
    }

    /**
     * Store references to sub pages as sublinks. Modify the path to the sublinks to point
     * to the epub pages directory. Do this only for subpages, which are e.g. in a table.
     *
     * @param uri
     * @param localName
     * @param qName
     * @param atts
     * @throws SAXException
     */
    protected void processHyperlink(String uri, String localName, String qName, Attributes atts) throws SAXException {
        BookPageType pageType = BookPageType.noBookPage;
        String href = getAttribute(atts, "href");
        try {
            pageType = getBookPageType(href);
            if (!pageType.equals(BookPageType.noBookPage))
                atts = substituteAttribute(atts, "href", nodeProvider != null ? nodeProvider.getReference(href) : href);

        } catch (RepositoryException re) {
            throw new SAXException(re);
        }

        if (pageType.equals(BookPageType.noBookPage)) {
            handler.startElement(uri, localName, qName, atts);
            return;
        }

        Resource resource = modifyReference(uri, localName, qName, atts, "href", new Substituter() {

            @Override
            public String substitute(String value) {
                return book.getNameBuilder().buildPageName(value);
            }
        });

        if (resource != null)
            page.getSublinks().add(new BookHyperlink(resource.getRepositoryRef(), pageType));

    }

    /**
     * Does the HTML page referenced by href belong to the book?
     * 
     * @param href
     * @return
     * @throws RepositoryException
     */
    protected BookPageType getBookPageType(String href) throws RepositoryException {
        if (isInvalid(href))
            return BookPageType.noBookPage;

        if (followLinks) {
            if (nodeProvider != null) {
                if (!nodeProvider.isRepositoryNode(href))
                    return BookPageType.noBookPage;
                else if (nodeProvider.isRootDescendant(href))
                    return BookPageType.inTree;
                else
                    return BookPageType.outsideTree;
            }

            return BookPageType.unknown;
        }

        if (nodeProvider != null && nodeProvider.isRootDescendant(href))
                return BookPageType.inTree;

        return BookPageType.noBookPage;
    }

    /**
     * Return the attribute value of the attribute with the specified
     * name from the attribute set.
     *
     * @param atts
     * @param attributeName
     * @return
     */
    protected String getAttribute(Attributes atts, String attributeName) {
        for (int i = 0; i < atts.getLength(); i++) {
            if (getName(atts.getURI(i), atts.getLocalName(i), atts.getQName(i)).equals(attributeName))
                return atts.getValue(i);
        }
        return null;
    }

    /**
     * Return a new attribute set with an additional attribute
     *
     * @param atts the old attribute set
     * @param name the name of the new attribute
     * @param value the value of the new attribute
     * @return the extended attribute set
     */
    protected Attributes addAttribute(Attributes atts, String name, String value) {
        AttributesImpl modified = new AttributesImpl(atts);
        modified.addAttribute("", name, name, "CDATA", value);

        return modified;
    }

    /**
     * Substitute an attribute value
     * 
     * @param atts
     * @param name
     * @param value
     * @return
     */
    protected Attributes substituteAttribute(Attributes atts, String name, String value) {
        AttributesImpl result = new AttributesImpl(atts);
        int index = result.getIndex(name);
        result.setValue(index, value);

        return result;
    }

    /**
     * Store references to sub pages as sublinks. Modify the path to the sublinks to point
     * to the epub pages directory.
     *
     * @param uri
     * @param localName
     * @param qName
     * @param atts
     * @throws SAXException
     */
    protected Resource modifyReference(String uri, String localName, String qName, Attributes atts, String attributeName, Substituter substituter) throws SAXException {
        Resource resource = null;

        AttributesImpl modified = new AttributesImpl();
        for (int i = 0; i < atts.getLength(); i++) {
            if (getName(atts.getURI(i), atts.getLocalName(i), atts.getQName(i)).equals(attributeName)) {
                resource = new Resource(atts.getValue(i));
                resource.setBookRef(substituter.substitute(atts.getValue(i)));
                modified.addAttribute(atts.getURI(i), atts.getLocalName(i), atts.getQName(i), CDATA, resource.getBookRef());
            } else
                modified.addAttribute(atts.getURI(i), atts.getLocalName(i), atts.getQName(i), CDATA, atts.getValue(i));
        }
        handler.startElement(uri, localName, qName, modified);

        return resource;
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
        String name = getName(uri, localName, qName);
        if (name.equals("link"))
            processCSS(uri, localName, qName, atts);
        else if (name.equals("script")) {
            if (isVisible())
                processScript(uri, localName, qName, atts);
        }
        else if (name.equals("title")) {
            charDestination = new StringBuffer();
            handler.startElement(uri, localName, qName, atts);
        } else if (name.equals("body")) {
            // make form body content invisible
            setVisible(false);
            handler.startElement(uri, localName, qName, atts);
        } else if (!isVisible()) {
            for (int i = 0; i < atts.getLength(); i++) {
                // make non form content visible again
                if (getName(atts.getURI(i), atts.getLocalName(i), atts.getQName(i)).equals("id") && atts.getValue(i).equals("content")) {
                    setVisible(true);
                    content = true;
                    firstContentElement = true;
                }
            }
        } else if (content) {
            atts = book.getAttributesFilter().filterAttributes(atts);
            
            if (name.equals("img"))
                processImage(uri, localName, qName, atts);
            else if (name.equals("a"))
                processHyperlink(uri, localName, qName, atts);
            else if (substitutions != null)
                processSubstitution(uri, localName, qName, atts);
            else {
                if (firstContentElement && page.getType().equals(BookPageType.outsideTree)) {
                    atts = addAttribute(atts, "id", "outsideTree");
                    firstContentElement = false;
                }

                handler.startElement(uri, localName, qName, atts);
            }
            path.push(name);
        } else
            handler.startElement(uri, localName, qName, atts);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if (content && !isVisible()) // substitution
            setVisible(true);

        if (isVisible())
            handler.endElement(uri, localName, qName);

        if (content) {
            if (path.size() > 0) {
                path.pop();

                if (path.size() == 0) {
                    // make form content invisible
                    setVisible(false);
                    content = false;
                }
            } 
        } else if (getName(uri, localName, qName).equals("body")) {
            // make rest visible again
            handler.endElement(uri, localName, qName);
            setVisible(true);
        } else if (getName(uri, localName, qName).equals("title")) {
            page.setName(charDestination.toString());
            charDestination = null;
        }
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        if (isVisible())
            handler.characters(ch, start, length);
        if (charDestination != null)
            charDestination.append(ch, start, length);
    }

    public HashMap<String, String> getSubstitutions() {
        return substitutions;
    }

    public void setSubstitutions(HashMap<String, String> substitutions) {
        this.substitutions = substitutions;
    }

}
